A Type Confusion Vulnerability Pattern in Windows RPC Servers
In this blog post, I will share a common type-confusion vulnerability scenario that exists in many RPC servers. I’ll walk through some of my thoughts while discovering and studying this class of issues, as well as certain technical details. These problems generally arise due to insufficient constraints in IDL definitions and the lack of proper validation inside RPC interfaces.
The story begins in early 2025 when I was researching the attack surface of Windows HTTP Services. During the review of various Windows HTTP components, I found that ssdpsrv hosts not only an HTTP service but also an RPC server. In the following research, I started auditing this RPC server and noticed that it exposed several different context-handle types, such as CONTEXT_HANDLE_TYPE and SYNC_HANDLE_TYPE, each managed by different RPC interfaces.
_SSDPOpenRpc
_SSDPCloseRpc
_InitializeSyncHandle
_RemoveSyncHandle
Context handles are extremely common in RPC. Conceptually, they work like indices. In rpcrt4, a context handle maps to a specific memory region that usually stores an object. This mechanism is similar to how object references are represented inside the Windows operating system.
When I examined this RPC service, one idea immediately came to mind: since a context handle is merely an index that represents a memory region, what would happen if I passed a CONTEXT_HANDLE_TYPE object into an interface that expects a SYNC_HANDLE_TYPE?
CVE-2025-48815
Inside ssdpsrv!_RemoveSyncHandle, the SYNC_HANDLE refers to a specific handle value that is opened by the InitializeSyncHandle interface. The interface creates a semaphore object:
__int64 __fastcall InitializeSyncHandle(_QWORD *a1)
{
[...]
Semaphore = CreateSemaphoreExW(0i64, 0, 0x7FFFFFFF, 0i64, 0, 0x1F0003u);
[...]
*a1 = v4;
return 0i64;
}
_RemoveSyncHandle then simply calls CloseHandle on this value:
BOOL __fastcall RemoveSyncHandle(void **a1)
{
[...]
result = CloseHandle(v1);
*a1 = 0i64;
[...]
}
However, if I pass a context handle returned by _SSDPOpenRpc into RemoveSyncHandle, the call still succeeds—even though the context points to a CONTEXT_HANDLE object instead of a sync handle. This is a straightforward type-confusion case. Interestingly, the handle returned by _SSDPOpenRpc actually points to the global variable g_lSsdpRef, which tracks a reference count:
__int64 __fastcall SSDPOpenRpc(_QWORD *a1)
{
[...]
++g_lSsdpRef;
*a1 = &g_lSsdpRef;
[...]
}
This means that by repeatedly calling _SSDPOpenRpc, I can cause the returned context handle to point to a reference-count value that matches any small integer handle value — e.g., 0x1C4, 0x200. When this value is passed to _RemoveSyncHandle, ssdpsrv ends up closing an arbitrary handle accessible in the process context.
FC_SUPPLEMENT and FC_BINDING_CONTEXT
Once I understood this behavior, I realized that many Windows RPC servers likely have similar setups — multiple context-handle types within the same interface. I quickly found other RPC servers with comparable patterns, but when testing them, RPC threw exception 0x6 (invalid_handle). This confused me: why does ssdpsrv allow cross-type usage while others reject it?
To investigate, I began reverse-engineering the RPC runtime.
A useful trick: exceptions inside RPC servers generally don’t break into the debugger by default. To capture exception 0x6, use:
sxe 00000006
This allows breaking at the exact moment the exception is thrown. By walking the call stack, I found the function NDRSContextUnmarshall2.
Reverse-engineering RPCRT4 is a massive task, so instead of analyzing every call in detail, I used a shortcut: I set a breakpoint on the same function inside ssdpsrv and compared the behavior against the other target RPC server.
Surprisingly, the breakpoint never triggered in ssdpsrv.
This meant the root cause was not inside that function. So I moved outward, comparing the surrounding logic, and eventually found the key difference: in the IDL of ssdpsrv, the context-handle descriptor begins with 0x70, while in the other RPC server, the value is 0x75:
.rdata:000000018003AE04 db 70h
.rdata:000000018003AE05 db 0EDh
.rdata:0000000140017850 db 75h
.rdata:0000000140017851 db 70h
This may indicate that different types exist among context handles. So I searched online for the type definitions of context handles, and in my colleague victorv’s blog (https://v-v.space/2023/09/06/rpc_readme/), I found some inspiration regarding these definitions. This blog also explains in detail how to identify parameter types in RPC, and I highly recommend interested readers to go through it.
In that post, victorv listed parameter types. Among them, 0x75 represents SUPPLEMENT, and 0x70 represents BIND_CONTEXT:
FC64_BIND_CONTEXT = 112 0x70
FC64_SUPPLEMENT = 117 0x75
With these structure types in mind, I noticed during reversing that when the incoming context handle is of type SUPPLEMENT, rpcrt4 will validate whether the context ID matches during unmarshalling. If it does not match, it throws an exception with error code 0x6. However, when the context handle is of type BIND_CONTEXT, rpcrt4 directly indexes the corresponding object and passes it into the RPC interface.
From binding_context to type confusion
At this point, things become much simpler. You only need to find an RPC server that has multiple context handles under the same interface, and this will give you a set of potential attack surfaces. During this process, I used James Forshaw’s tool NtObjectManager (https://github.com/googleprojectzero/sandbox-attacksurface-analysis-tools/tree/main/NtObjectManager). NtObjectManager can parse the RPC server’s IDL and clearly distinguish between FC_BINDING_CONTEXT and FC_SUPPLEMENT, so I only needed to filter RPC servers whose interfaces contain multiple occurrences of "[out] /* FC_BINDING_CONTEXT */".
CVE-2025-53143
Crash Dump:
0:034> r
rax=00000283529a8f50 rbx=00000283529a8f50 rcx=00007ffe440edb60
rdx=0000000000000001 rsi=00007ffe440eba28 rdi=0000000000000001
rip=00007ffe44027373 rsp=000000a4c2c7e9d0 rbp=0000000000000011
r8=7ffffffffffffffc r9=000000a4c1bb1000 r10=0000000000000000
r11=000000a4c2c7e830 r12=0000000000000001 r13=000000000000000e
r14=000000a4c2c7ee90 r15=0000000000000000
iopl=0 nv up ei pl nz na pe nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010202
MQQM!CTransaction::StartPrepareRequest+0x5b:
00007ffe`44027373 834b1420 or dword ptr [rbx+14h],20h ds:00000283`529a8f64=????????
Stack Trace:
0:034> k
# Child-SP RetAddr Call Site
00 000000a4`c2c7e9d0 00007ffe`440265e6 MQQM!CTransaction::StartPrepareRequest+0x5b
01 000000a4`c2c7ea10 00007ffe`44037e01 MQQM!CTransaction::InternalCommit+0x62
02 000000a4`c2c7ea40 00007ffe`43ff81ab MQQM!QMDoCommitTransaction+0xcd
03 000000a4`c2c7ea70 00007ffe`43fc5685 MQQM!qmcomm_v1_0_S_QMCommitTransaction+0xb
04 000000a4`c2c7eaa0 00007ffe`68113882 RPCRT4!Ndr64StubWorker+0x862
05 000000a4`c2c7ead0 00007ffe`680c599c RPCRT4!NdrServerCallAll+0x3c
...
CVE-2025-54104
Crash Dump:
0:013> r
rax=0000000000000000 rbx=0000000002000200 rcx=00007fffe5104ad8
rdx=00000080007fe188 rsi=00000080007fe188 rdi=0000008001ff9b10
rip=00007fffe50c3943 rsp=00000080007fe120 rbp=00000080007fe1f0
r8=00000080007fe088 r9=0000000000000000 r10=0000000000000000
r11=0000000000000246 r12=0000000000000000 r13=0000008001ff9c78
r14=00007fffe5104000 r15=0000008001ff9c60
iopl=0 nv up ei pl zr ac po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010256
fwbase!FwStringCopy+0x53:
00007fff`e50c3943 66833c4300 cmp word ptr [rbx+rax*2],0 ds:00000000`02000200=????
Stack Trace:
0:013> k
# Child-SP RetAddr Call Site
00 00000080`007fe120 00007fff`d90fd4cd fwbase!FwStringCopy+0x53
01 00000080`007fe160 00007fff`d90f253b mpssvc!SvrImpl_FWUnregisterProduct+0x4d
02 00000080`007fe1b0 00007fff`e983de83 mpssvc!RPC_FWUnregisterProduct+0x9b
03 00000080`007fe1e0 00007fff`e9841e83 RPCRT4!Invoke+0x73
...
Of course, this type of vulnerability is not universally present in RPC services. In my research, I found that when IDL defines a context handle as BIND_CONTEXT, many interfaces perform explicit validation—for example, checking the object type within the object value. If such validation is missing, type confusion becomes possible.
Therefore, in IDL definitions:
- If the context handle type is SUPPLEMENT, rpcrt4 will automatically enforce checks.
- If the type is BIND_CONTEXT, the interface function must explicitly validate the incoming object type to prevent type confusion.
Reference
https://v-v.space/2023/09/06/rpc_readme/
https://github.com/googleprojectzero/sandbox-attacksurface-analysis-tools/tree/main/NtObjectManager